/*
 * Copyright 2011 Jin Kwon <jinahya at gmail.com>.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package com.googlecode.flac4j.format.metadata;


import com.googlecode.flac4j.format.FormatHelper;

import com.googlecode.jinahya.io.BitInput;
import com.googlecode.jinahya.io.BitOutput;
import com.googlecode.jinahya.util.BitAccessible;

import java.io.IOException;

import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;


/**
 *
 * @author Jin Kwon <jinahya at gmail.com>
 */
@XmlType(propOrder = {"offset", "number", "ISRC", "audio", "preEmphasis",
                      "indices"})
public class Track implements BitAccessible {


    /** GENERATED. */
    private static final long serialVersionUID = -5609925170244629380L;


    public static final int ISRC_LENGTH = 12;


    public static final int CD_DA_LEAD_OUT = 170;


    public static final int NON_CD_DA_LEAD_OUT = 255;


    protected int getLengthInBytes() {
        return 36 + (indices.size() * Index.LENGTH_IN_BYTES);
    }


    @Override
    public void read(final BitInput input) throws IOException {

        setOffset(input.readLong(64));

        setNumber(input.readUnsignedInt(8));

        ISRC = new byte[ISRC_LENGTH];
        FormatHelper.readBytes(input, ISRC);

        setAudio(!input.readBoolean());

        setPreEmphasis(input.readBoolean());

        // ------------------------------------------------------------ RESERVED
        input.readUnsignedInt(6);
        for (int i = 0; i < 13; i++) {
            input.readUnsignedInt(8);
        }
        // ------------------------------------------------------------ RESERVED

        indices.clear();
        final int indexCount = input.readUnsignedInt(8);
        for (int i = 0; i < indexCount; i++) {
            final Index index = new Index();
            index.read(input);
            addIndex(index); // IllegalArgumentException
        }
    }


    @Override
    public void write(final BitOutput output) throws IOException {

        output.writeLong(64, getOffset());

        output.writeUnsignedInt(8, getNumber());

        FormatHelper.writeBytes(output, getISRC());

        output.writeBoolean(!isAudio());

        output.writeBoolean(getPreEmphasis());

        // ------------------------------------------------------------ RESERVED
        output.writeUnsignedInt(6, 0x00);
        for (int i = 0; i < 13; i++) {
            output.writeUnsignedInt(8, 0x00);
        }
        // ------------------------------------------------------------ RESERVED

        output.writeUnsignedInt(8, indices.size());
        for (Index index : indices.values()) {
            index.write(output);
        }
    }


    // ---------------------------------------------------------------- cuesheet
    /**
     * Returns cuesheet.
     *
     * @return cuesheet.
     */
    public Cuesheet getCuesheet() {
        return cuesheet;
    }


    /**
     * Sets cuesheet.
     *
     * @param cuesheet cuesheet.
     */
    public void setCuesheet(final Cuesheet cuesheet) {

        if (cuesheet == null) {
            throw new NullPointerException("cuesheet");
        }

        this.cuesheet = cuesheet;
    }


    // ------------------------------------------------------------------ offset
    /**
     * Returns offset.
     *
     * @return offset
     */
    public long getOffset() {
        return offset;
    }


    /**
     * Sets offset.
     *
     * @param offset offset
     */
    public void setOffset(long offset) {

        if (offset < 0L) {
            throw new IllegalArgumentException("offset(" + offset + ") < 0L");
        }

        this.offset = offset;
    }


    // ------------------------------------------------------------------ number
    /**
     * Returns number.
     *
     * @return number.
     */
    public int getNumber() {
        return number;
    }


    /**
     * Sets number.
     *
     * @param number number.
     */
    public void setNumber(final int number) {

        if (number < 0) {
            throw new IllegalArgumentException("number(" + number + ") < 0");
        }

        // 0 is not allowed to avoid conflicting with the CD-DA spec, which
        // reserves this for the lead-in.
        if (number == 0) {
            throw new IllegalArgumentException("number(" + number + ") == 0");
        }

        this.number = number;
    }


    // -------------------------------------------------------------------- ISRC
    /**
     * Returns ISRC.
     *
     * @return ISRC
     */
    public byte[] getISRC() {
        return Arrays.copyOf(ISRC, ISRC.length);
    }


    /**
     * Sets ISRC.
     *
     * @param ISRC ISRC
     */
    public void setISRC(final byte[] ISRC) {

        if (ISRC == null) {
            throw new NullPointerException("null ISRC");
        }

        if (ISRC.length != ISRC_LENGTH) {
            throw new IllegalArgumentException("ISRC.length != " + ISRC_LENGTH);
        }

        for (int i = 0; i < ISRC.length; i++) {
            if (ISRC[i] < 0) {
                throw new IllegalArgumentException("ISRC[" + i + "] < 0");
            }
        }

        this.ISRC = Arrays.copyOf(ISRC, 12);
    }


    // ------------------------------------------------------------------- audio
    /**
     * Returns audio.
     *
     * @return audio.
     */
    public boolean isAudio() {
        return audio;
    }


    /**
     * Sets audio.
     *
     * @param audio audio.
     */
    public void setAudio(final boolean audio) {
        this.audio = audio;
    }


    // ------------------------------------------------------------- preEmphasis
    /**
     * Returns preEmphasis.
     *
     * @return preEmphasis
     */
    public boolean getPreEmphasis() {
        return preEmphasis;
    }


    /**
     * Sets preEmphasis.
     *
     * @param preEmphasis preEmphasis
     */
    public void setPreEmphasis(final boolean preEmphasis) {
        this.preEmphasis = preEmphasis;
    }


    // ----------------------------------------------------------------- indices
    /**
     * Clears indices.
     */
    public void clearIndices() {
        indices.clear();
    }


    /**
     * Returns index count.
     *
     * @return number of index added so far
     */
    public int getIndexCount() {
        return indices.size();
    }


    /**
     * Check if indices is empty.
     *
     * @return true if indices is currently empty; false otherwise
     */
    public boolean isIndicesEmpty() {
        return indices.isEmpty();
    }


    /**
     * Returns the next sequential index number.
     *
     * @return next index number
     */
    public int getNextIndexNumber() {

        if (indices.isEmpty()) {
            return 0;
        }

        return indices.lastKey() + 1;
    }


    /**
     * Adds given <code>index</code>.
     *
     * @param index index to add
     */
    public void addIndex(final Index index) {

        if (index == null) {
            throw new NullPointerException("null index");
        }

        // The first index in a track must have a number of 0 or 1, and
        // subsequently, index numbers must increase by 1.
        final int indexNumber = index.getNumber();
        if (indices.isEmpty()) {
            if (indexNumber != 0 && indexNumber != 1) {
                throw new IllegalArgumentException(
                    "illegal first index.number: " + indexNumber);
            }
        } else {
            if (indexNumber != indices.lastKey() + 1) {
                throw new IllegalArgumentException(
                    "illegal index.number: " + indexNumber);
            }
        }

        index.setTrack(this);

        indices.put(index.getNumber(), index);
    }


    /**
     * Returns an unmodifiable map of indices.
     *
     * @return an unmodifiable map of indices.
     */
    public Map<Integer, Index> getIndices() {

        return Collections.unmodifiableSortedMap(indices);
    }


    /** parent cuesheet. */
    @XmlTransient
    private Cuesheet cuesheet;


    /** track offset in samples. */
    @XmlElement(required = true)
    private long offset;


    /** track number. */
    @XmlElement(required = true)
    private int number;


    /** ISRC. */
    @XmlElement(required = true)
    private byte[] ISRC;


    /** audio. */
    @XmlElement(required = true)
    private boolean audio;


    /** preEmphasis. */
    @XmlElement(required = true)
    private boolean preEmphasis;


    /** indices. */
    @XmlElement(name = "index")
    @XmlJavaTypeAdapter(IndicesAdapter.class)
    private final SortedMap<Integer, Index> indices =
        new TreeMap<Integer, Index>();


}

